Repository navigation
npu-pack: import the .fconst MLP section (gate|up fused, then down_proj) - #842
Conversation
PR Reviewer Guide 🔍(Review updated until commit c4d8dfc)Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at c84110ae — one item, and it is a one-line cast: the aggregate CodeQL check is red on a new
alert in the file this PR adds. Everything else here reproduces on the shipped pack, including the parts I could check
independently of the importer's own code.
The blocker. CodeQL alert #70, rule cpp/integer-multiplication-cast-to-long (high),
tests/npu_pack_import_test.cpp:657:
check(r.mlp_groups_total == layers * (p.dims.intermediate / 64 + p.dims.hidden / 64),r.mlp_groups_total is long long but layers is int, so the multiply happens in int and is converted afterwards
(the values here are 24 x 90, so nothing overflows — the query is right about the form, not about this pack).
Fix: int64_t(layers) * (...). #844 carries the same line, so fixing it in this branch fixes both; I re-verdict
both the moment a head moves.
Built and run here (fresh Release/ninja/gcc-16 configure, -DONEBIT_OGA_PACK=<the 0.5B pack>):
ninja npu_pack_import_test onebit rc 0, the only warning the pre-existing nlohmann one in app/serve.cpp;
ctest -R npu_pack_import -> 2/2 (npu_pack_import 0.04 s, npu_pack_import_oga 6.89 s). My own import of the
shipped Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K:
imported 290 tensors, 67088 q4_1 tiles
self-check: lm_head vs the pack's fp16 embedding corr 0.999999 rel_err 0.000700
controls: nibble-swapped corr -0.0008, no zero-point corr 0.8788 (both must fail)
attention: 768/768 .fconst tiles survive decode->encode byte for byte; 96 projections + 72 bias tensors
MLP: 2160/2160 .fconst groups survive decode->encode byte for byte; 72 projections imported
fconst: stride 8708288, second header 1115968 (measured from the eps anchors); 48/48 RMSNorm anchors found
incomplete: 1 tensors
so 290 / 67,088 / 2160-of-2160 / one refusal are all the PR's numbers, and the negative controls fire.
Independent of pack_import.cpp, I re-derived the load-bearing claims in Python:
tests/golden/oga_mlp_gu_group.binis byte-identical to the pack's bytes at offset 1,117,824, and that offset is
exactly where the MLP section has to start:1,115,968(attention payload end)+ 1,856(norm envelope). Then
1,117,824 + 5,214,208 + 64 + 2,376,192 = 8,708,288= the stride the epsilon anchors give on both files, so the
geometry identity really is an identity for this pack and not a fitted constant.- my own transcription of the 4,288-byte subvolume (§7.17: quants at
(cp%4) + kk*4 + (cp/4)*512, 64 zero bytes at
4,096, 64 bf16 scales at 4,160, subvolumex2 + 2*(kblock+1), bias subvolumes 0 and 1) decodes that unit, splits
gate/up at the 32-column chunks, rebuilds the container tiles with the K -> 1,024 column padding and the bf16 zero
rounding, and gets 16 tiles byte-identical tooga_mlp_gu_q4_1.bin(sha256422e84a0a3a8af54…on both sides).
Bias subvolumes are all zero, codes 0..15, scales 0.0030..0.0261, zero points 2..12 — plausible for a 4-bit MLP. - tile arithmetic: 24 layers x (608 gate + 608 up + 532 down) = 41,952 = 67,088 - 25,136, and 218 + 72 = 290.
- the scope statement's refusals name the doc's own numbers on this box:
Qwen3-1.7B-> "the MLP section ends at 25,563,328 B in a 27,931,456-byte layer block" (2,368,128 unaccounted),
Qwen3-4B-> "49,562,816 B in 55,308,096" (5,745,280), both exactly FACTS §7.17. (Llama-3.2-1Brefuses earlier,
for a pre-existing reason: fewer than 6 epsilon headers, so even the attention payload is not located — not this PR,
but worth knowing that pack is not in the "the MLP is the only gap" class.)
One thing the committed tests cannot catch, worth a follow-up rather than a gate item: a global gate/up swap
would pass everything here — the golden's split is itself the assumption, and the controls only catch a swap inside
one column block. The independent evidence for the assignment is the lane's prefill cross-decode (§7.17), and the
prefill .fconst that carries it ships in this same pack, so a CI test could compare the two physical layouts directly
(layer 0 alone would be enough) instead of relying on a measurement recorded in a golden JSON.
I took no NPU ticket: nothing here needs the device, and the owner's serve holds the lock on :18097.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT stays CHANGES, and this supersedes my earlier list: the zero-point nibble pairing this decoder implements is
not the file's, so every q4_1 tile it imports carries another row's zero point in half of its rows. The CodeQL item is
now the small one.
What the code does. decode_tile pairs the zero bytes into rows by halves —
const int n0 = r0 + 2 * cp; // codes: byte cp -> rows 2cp, 2cp+1
...
const uint8_t z = s[zeros_off + i];
t.zp[size_t(r0 + i) ...] = z & 0x0F; // zero points: byte i -> rows i and i+nsubv/2
t.zp[size_t(r0 + nsubv / 2 + i) ...] = z >> 4;— while the codes in the same subvolume use even/odd (n0 = r0 + 2*cp). One of the two has to be wrong, and it is the
zero points.
Measured, one file is enough. A correctly paired zero point sits where its 128 codes cluster, so per (row, 128-block)
mean(code) should track zp[row][block]. On the shipped pack's own bytes (~/lb/tprs-842/zp_pairing.py, prints all
four shapes):
pairing corr |mean(code)-zp|
decode attn tile L0 (2,176) halves +0.0229 2.3787
decode attn tile L0 (2,176) evenodd +0.9970 0.1605
decode fused gate|up L0 (4,288) halves +0.0328 0.9861
decode fused gate|up L0 (4,288) evenodd +0.9621 0.2111
prefill gate L0 (2,176) halves +0.0620 1.0203
prefill gate L0 (2,176) evenodd +0.9665 0.2107
prefill down L0 (2,176) halves +0.0598 0.9468
prefill down L0 (2,176) evenodd +0.9613 0.2102
a shuffled zp gives corr ~0 and |diff| ~1.01, so the halves reading is indistinguishable from noise on every shape
while even/odd is ~0.96-0.997.
And the two physical layouts agree only under even/odd. Decoding the fused 4,288 gate|up group and the prefill file's
separate gate/up tensors (~/lb/tprs-842/crosscheck_prefill.py): codes identical 24/24 layers, scales identical 24/24,
and zero points identical 24/24 under even/odd, 0/24 under halves. The row correspondence itself is pinned by the
codes (every prefill gate row is found at fused row 0..31, 64..95, …, i.e. the 32-column interleave this PR claims), so
the zero-point result is not an alignment artefact.
Why nothing caught it: encode_tile mirrors decode_tile, so the 768/768 and 2160/2160 "survive decode→encode byte
for byte" checks are blind to a permutation that is applied twice; and both committed goldens
(oga_mlp_gu_q4_1.bin here, oga_attn_tile.bin from #829) were generated by this same decoder. The 32-byte field is
(col % Nsubv)/2 in FACTS §7.17 — byte i covers rows 2i and 2i+1, exactly what the codes do — while §7.13's
"rows 0..15 low, 16..31 high" is the reading the code inherited and the shipped bytes contradict.
Fix: pair the zero nibbles like the codes (r0 + 2*i, r0 + 2*i + 1) in decode_tile and encode_tile, then
re-generate both goldens (tools/oga_mlp_gu_golden.py, and tools/oga_attn_tile_golden.py for the attention one) — they
are currently fixtures for the bug, and the round-trip check will still pass afterwards, so the numbers to look at are the
container's tiles, not the round trip. Magnitude for triage: with a wrong zero point the row's weights are offset by
Δzp × scale, a few int4 steps of a 16-step range, so it is a wrong model rather than a crash — which is why the 291-tensor
import, the container read-back and every gate so far looked healthy.
Same defect, not this PR's doing: origin/main has it at npu/pack_import.cpp:622-624 (merged with the attention import
in #829), so the containers built since carry it for q/k/v/o as well. Fixing it here fixes both paths; the attention
goldens then need the same regeneration. I have filed it as an issue so it is not lost if this PR is rebased.
…he codes (closes #849) (#879) decode_tile()/encode_tile() read the zero-point nibbles of each 2,176-byte subvolume as row i / row i+16, while the codes of the same subvolume pair row 2i / 2i+1. The shipped pack's own bytes say even/odd: a correctly paired zero point sits where its 128 codes cluster, so per (row, K block) mean(code) must track zp -- +0.9977 under even/odd, -0.0227 under halves, on tile 2 of layer 0 taken straight out of the pack's .fconst. Every container imported since the attention payload landed (#829) therefore carried another row's zero point in half of its rows, for q/k/v/o. w = (code - zp) * scale, so the affected rows were offset by dzp x scale -- a few int4 steps. Silent: decode -> encode is invariant under a permutation applied on both sides, and the committed goldens were generated by this same decoder, so 768/768 attention tiles and 2160/2160 MLP groups still "passed". The fix is the two 2-line blocks. What comes with it: * tools/oga_attn_tile_golden.py carried the same halves reading (it is the independent numpy implementation the fixture is checked against, so it did not catch it either); re-paired, and tests/golden/oga_attn_tile_q4_1.bin regenerated from the pack. The raw tile fixture (oga_attn_tile.bin) is unchanged -- it is the vendor's bytes. * two assertions in tests/npu_pack_import_test.cpp, because the round trip cannot see this class: (a) a structural check on a tile whose nibbles are all distinct (byte i must give rows 2i/2i+1), and (b) the code-cluster statistic on the shipped tile (corr > 0.9). With the pairing reverted, the test fails on both plus the container golden; with the fix, all npu-pack checks pass. * a note in TileW's header comment so the MLP decoder on #842 does not repeat it. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Verdict stays CHANGES, and the reason has changed shape since my 2026-11-02 review: main fixed half of it.
#879 (f3d9f6c0c, merged 10:45Z today) landed the zero-point pairing fix on main — decode_tile()/encode_tile() now read row 2i and 2i+1 from byte i ("The zero-point nibbles pair with rows exactly as the codes do (even/odd) … corr(zp, mean code) +0.998 vs −0.023 for halves"). This branch still carries the halves pairing:
$ gh pr diff 842 | grep -c 'r0 + 16 + i'
2
and that is exactly where it now conflicts (mergeable: CONFLICTING, untouched since 2026-11-02T19:27Z). So the MLP payload work itself is not what is blocking — the duplicate of a fix that already landed is.
To make this mergeable:
git fetch origin main && git rebase origin/main, and when thezphunks conflict, take main's side: the pairing this branch implements is the one #849 was filed about.- Regenerate the
oga_mlp_gu*goldens. They were produced with the halves reading, so they encode the wrong pairing; after the rebase they will fail against a correct decoder, and re-baselining them is the intended outcome, not a workaround. - Then #844 (
1bit/671-oga-final-norm) can follow — it is stacked on this branch's head and would otherwise carry this PR's commits with it.
I re-verdict within minutes of the push — the checks are the golden regeneration plus npu_pack_import_test on the four OGA packs (Llama-3.2-1B, Qwen3-4B, Qwen3-1.7B, Qwen2.5-0.5B), which are all on this box.
The MLP section of a layer block was the last part of an OGA pack the importer
refused: 72 projections on Qwen2.5-0.5B. It is now decoded, byte-verified and
imported, and the remaining refusal is the final norm.
Geometry, derived from the writer's own headers (mladfQuantMatrix /
mladfWgtSubv in the wheel's ops_common/mladf_matmul_matrix.hpp, concat_gate_up
in matmulbias_weights_gateup.cpp) and then MEASURED, not assumed:
gate|up one fused hidden x 2*intermediate tensor, rows interleaved in
32-column chunks, in 128-column groups of 4,288-byte subvolumes
(the same struct at Nsubv 64: 4,096 quants + 64 zeros + 64 bf16
scales)
...then one 64-byte parameter subvolume, the same bytes in every block
down intermediate x hidden in 64-column groups of the 2,176-byte
subvolume the attention payload already uses
The three parts have to fill the block stride exactly, and that identity (not a
constant) is what decides whether this pack is one the decoder understands: on
Qwen2.5-0.5B they end at 8,708,288 B, the measured stride, to the byte. Qwen3-1.7B
and Qwen3-4B do not add up and refuse by name with the size they did add up to.
Decoder changes are the same code at a second width: TileW gains `rows` (64 or
128 columns), the subvolume size, its zero/scale offsets and its nibble index
follow from it, and gu_half() splits a fused 128-column group into the two
projections (gate = rows 0..31 and 64..95, up = 32..63 and 96..127).
Verified, all of it on the shipped pack:
* 2160/2160 MLP groups decode and re-encode to the pack's exact bytes
(24 layers x 76 gate|up units + 24 x 14 down groups), the same contract the
attention tiles are held to;
* every weight in those groups agrees byte for byte with an INDEPENDENT decode
of the same layer: the decode program stores gate/up fused at Nsubv 64 while
the prefill program stores them separately at Nsubv 32, and the two agree on
codes, scales and zero points for all 24 layers x {gate, up, down};
* a new CI fixture (tests/golden/oga_mlp_gu_group.bin, one 68,608-byte unit
copied verbatim from the pack by tools/oga_mlp_gu_golden.py) pins the decode
and the split against the vendor writer's own bytes with no vendor stack
present, including the two controls that must fail;
* container: 290 tensors / 67,088 q4_1 tiles (was 218 / 25,136), and the
engine's own Model opens it.
The bf16 parameter bound is unchanged and reported: zero points are bf16-exact
for 52.9 % of the MLP's 32-column groups, 48.7 % of the attention ones.
…s class at 128 rows) The MLP section decodes 4,288-byte subvolumes, i.e. 64 rows per half instead of attention's 32, so #849's two assertions are repeated at that width: structural (distinct nibbles land on rows 2i / 2i+1) and statistical (corr(zp, mean code) on the pack's own unit, which is the only one of the two a wrong pairing cannot pass). The golden's numpy reference carried the halves reading, like the attention tool did, so it could not have caught this; re-paired, goldens regenerated from the pack.
c84110a to
9be1d17
Compare
|
Rebased onto What changed, and why each part was needed:
That last one is the real evidence: even/odd scores above 0.9 on the MLP payload too, so #849's rule holds at the fused width, not just for attention. Verification at |
|
Preparing review... |
|
Persistent review updated to latest commit bb3fffb |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at bb3fffbba — the pairing item I raised is fixed, but the pack leg regresses three of the four real OGA packs on this box, and this PR's own test is what says so. ctest 6/6 cannot see it, and that is the trap worth closing in this PR.
Measured, same binary, same packs, same invocation (npu_pack_import_test "" <pack>):
| pack | main ae0ee19 |
this head bb3fffbba |
|---|---|---|
Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K |
all checks passed | all checks passed |
Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K |
all checks passed | 3 check(s) failed |
Qwen3-1.7B_rai_1.8.0_npu_4K |
all checks passed | 3 check(s) failed |
Qwen3-4B_rai_1.8.0_npu_4K |
all checks passed | 3 check(s) failed |
The three failures on each of the three packs:
FAIL pack: the MLP projections (refused only when their geometry does not add up) plus the final norm, got 113
FAIL pack: every refusal names where the bytes are and the fact that is missing
FAIL pack: the 4 attention projections per layer are planned from the measurement
The 113 is the informative one: the test expects layers*3 + 1 refusals when the MLP geometry does not add up (:654, 1B = 49), and this head produces 4*16 + 3*16 + 1 = 113, i.e. the four attention projections per layer are being refused as well — which is why attn_note.empty() == (attn_tiles > 0) (:661) also breaks. Some of those refusals additionally do not name fconst/layer block (:657-660). The 4B gives 361 = 36*10 + 1, so the per-layer count is not even constant across packs. ~/ogaload/oga8/c849, c849s and the 0.5B pack still pass, so whatever was validated is the small geometry.
Why ctest 6/6 is green: the pack leg is skipped unless a pack directory is passed — npu_pack_import_test "" <pack>; with no argument the binary prints skip pack: no pack directory given and the checks above never run (tests/npu_pack_import_test.cpp:840-851). So the self-test that gates CI exercises the crafted cases only. One way to close it for good: let the pack leg take the packs from an env list (as npu_elf_args_test does with its ELFs) and register a second add_test for the packs that exist, or drop the argument requirement and skip per pack.
What I confirmed as done: the zero-point pairing is now the even/odd reading #879 landed on main — npu/pack_import.cpp:690 iterates i < nsubv / 2 over "rows 2i, 2i+1, the pairing decode_tile() reads", identical to main's; npu_pack_import_test at this head reproduces my earlier fconst eps output for both epsilons; the two oga_mlp_gu* goldens are in the diff (regenerated, not carried); docs/npu.md documents the new section. The refusal wording nit is not a style point here — it is the second failing check.
Nit while you are in there: npu/pack_import.cpp:693 puts two statements on one line (for (…) { const uint16_t h16 = …).
|
Current with |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 3d1b15da2 — the head is Merge remote-tracking branch 'origin/main' on top of bb3fffbba, i.e. no code change, and the pack leg is red on the same three packs. A merge-only re-test cannot clear this, and "all npu-pack checks passed" is what the skipping invocation prints.
Reproduced here at this exact head (built from it, ninja npu_pack_import_test, then run):
$ ./npu_pack_import_test # what ctest runs
skip pack: no pack directory given
all npu-pack checks passed <- the green you saw
$ ./npu_pack_import_test "" /home/bcloud/models/oga/Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K
3 check(s) failed
| pack | main ae0ee19 |
bb3fffbba |
3d1b15da2 |
|---|---|---|---|
| Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K | passed | passed | passed |
| Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K | passed | 3 failed | 3 check(s) failed |
| Qwen3-1.7B_rai_1.8.0_npu_4K | passed | 3 failed | 3 check(s) failed |
| Qwen3-4B_rai_1.8.0_npu_4K | passed | 3 failed | 3 check(s) failed |
The three, unchanged: the MLP projections … plus the final norm, got 113 (expected layers*3+1 = 49 at 16 layers, so four attention projections per layer are being refused too), every refusal names where the bytes are and the fact that is missing, the 4 attention projections per layer are planned from the measurement.
Why the shortcut is structural, not a lapse: CMakeLists.txt:596 registers add_test(NAME npu_pack_import COMMAND npu_pack_import_test ${CMAKE_SOURCE_DIR}/tests/golden) — the golden dir is argv[1], no pack is argv[2], so the pack leg self-skips and the binary still exits 0 with all npu-pack checks passed. The one test that does pass a real pack is optional: ONEBIT_OGA_PACK (:597-602), unset by default. So ctest, and any invocation without a pack path, will always print that line.
To clear it: fix the three packs (or scope the refusals so a pack without the MLP section refuses exactly layers*3+1 and keeps its attention tiles), then show the pack-leg run — the one command above is enough, and c849/c849s/0.5B passing is what a same-geometry check would report either way.
…leg as a pass (engine#842 review) The rebase onto main took this branch's simpler refusal accounting and dropped main's attention-aware one, so the three packs whose attention payload is not a whole number of tiles (Llama-3.2-1B, Qwen3-1.7B, Qwen3-4B) went red: 113/281/361 refusals where the branch expected layers*3+1. Restored main's per-class counts (norm / tail / MLP / attention / other) and kept the MLP expectation, so a pack without an MLP section refuses exactly layers*3+1 and keeps its attention tiles. Why it stayed invisible: ctest runs the test with the golden dir only and no pack, the pack leg self-skips, and the verdict line still said "all npu-pack checks passed". It now says whether the pack leg ran, and the skip is printed as a skip. Verified on all four real packs on this box (rc 0 each, "including the pack leg"): Llama-3.2-1B-Instruct, Qwen2.5-0.5B-Instruct, Qwen3-1.7B, Qwen3-4B; ctest 6/6; copyright rc 0.
|
Preparing review... |
7f5d2ea to
a4afb54
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Runtime MLP refusals can leave saved manifests incorrectly marking omitted projections as importable, while ineffective assertions and a removed epsilon-test invocation weaken regression coverage.
4 open issues
What changed in this PR
This PR extends the NPU pack importer to decode fused gate/up and down-projection MLP weights, leaving only the final norm refused on the supported Qwen2.5-0.5B pack.
Changes:
- Generalizes tile decoding to support both attention and fused MLP widths, with byte-for-byte round-trip checks.
- Adds MLP geometry validation, import reporting, and golden-fixture checks.
- Documents the MLP layout, supported geometry, and updated import totals.
| File | Description |
|---|---|
tools/oga_mlp_gu_golden.py |
Generates and checks MLP golden fixtures from a pack. |
tests/npu_pack_import_test.cpp |
Adds MLP decode, split, and import checks; clarifies skipped pack runs. |
tests/golden/oga_mlp_gu.json |
Records fixture provenance and fused gate/up layout. |
npu/pack_import.h |
Adds variable-width tile APIs and MLP planning/report fields. |
npu/pack_import.cpp |
Implements MLP geometry validation, decoding, splitting, and import. |
docs/npu.md |
Documents MLP support, layout, limitations, and import totals. |
Review effort: Balanced
|
Current at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 08fc9b03e — Merge remote-tracking branch 'origin/main', pack leg green on all four real OGA packs at this head, and the CodeQL overflow thread is answered and resolved (CodeQL SUCCESS).
Qwen2.5-0.5B rc=0 all npu-pack checks passed (including the pack leg)
Llama-3.2-1B rc=0 all npu-pack checks passed (including the pack leg)
Qwen3-1.7B rc=0 all npu-pack checks passed (including the pack leg)
Qwen3-4B rc=0 all npu-pack checks passed (including the pack leg)
One thing this run turned up, and it is in the test, not the importer: tests/npu_pack_import_test.cpp:719 writes to a fixed path — fs::temp_directory_path() / "oga_pack_import_test" — and fs::remove_all()s it at :720 and :725. Two runs at the same time (another lane in the same pack family, or two arms of the same check) delete each other's directory mid-write, and the failure reads like a pack problem:
Llama-3.2-1B rc=1 FAIL exception: filesystem error: cannot copy file: No such file or directory
[…/Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K/tokenizer.json] [/tmp/oga_pack_import_test/tokenizer.json]
Qwen3-1.7B rc=1 FAIL exception: cannot write /tmp/oga_pack_import_test/model.q4nx
Qwen3-4B rc=1 FAIL exception: cannot write /tmp/oga_pack_import_test/model.q4nx
I hit exactly that on my first pass at this head — two of four packs red, no FAIL pack: line anywhere — and the same four packs were green on a re-run, and green twice more in a row when nothing else was using the row. It is why my earlier "3 of 4 failed" and your "4 of 4" can both be honest for the same head. A per-process directory closes it:
const std::string out = (fs::temp_directory_path() /
("oga_pack_import_test-" + std::to_string(::getpid()))).string();Not a merge blocker from me (the importer is fine; this is the row's own plumbing), but it is a false-FAIL source in a gate row that several lanes run concurrently tonight, and it is one line.
… controls (review) Four review threads on #842, all right: 1. The container's manifest came only from the plan, so a tensor the IMPORT refused at run time (a group that does not survive decode->encode, a nonempty bias subvolume, a missing tail envelope) was absent from the container while `incomplete` still listed only the planned refusals. Under --allow-incomplete that reported `incomplete: []` for weights that were skipped. The runtime refusals are now merged in: each is attributed to the tensor it names (`refusal` + `imported: false`) and appended to `incomplete`. 2. The even/odd structural checks assigned the accumulator inside the innermost loop, so an early wrong pair was hidden by a later right one and only the last pair of each K block was checked. Accumulated now -- in the MLP check and in the attention check, which has had the same defect since #849. 3. The gate/up swap control compared a gate-only tile stream against the gate+up one, so it passed on length alone. Both halves of the swapped unit are built before comparing. 4. #842's arrival dropped the only call of test_fconst_eps(), so neither ctest entry exercised the per-pack epsilon anchors any more. Restored. Also per-process scratch dir for the pack leg: two concurrent runs shared /tmp/oga_pack_import_test and one died with "cannot write .../model.q4nx" (measured today). Verified: end-to-end manifest proof with a pack whose MLP bias byte is non-zero -- the container it wrote lists the refusal in `incomplete` and marks mlp.gate_proj.weight imported=false with its reason; all four real packs pass the pack leg (rc 0); ctest 6/6; copyright rc 0.
|
Persistent review updated to latest commit 2e9c5fa |
…states, and test the tail envelope Four review threads on #844: 1. The prefill cross-check could abort an otherwise valid import: open_fconst() throws on an empty or unreadable prefill .fconst, before the "no readable tail envelope" note is reached. It is wrapped now -- a mapping failure leaves the cross-check unavailable and the import proceeds from the Token_Token copy. 2. `final_norm_crosscheck == false` conflated three different states, and the CLI reported "taken from the tail envelope (one copy to check)" for all of them -- including a norm that was REFUSED and never imported. The report now carries final_norm_imported and prefill_tail_seen, and the line reads "REFUSED (no readable tail envelope)" / "identical in both .fconst tails" / "taken from the Token_Token tail -- the two copies DISAGREE" / "taken from the tail envelope (the pack ships no prefill copy)". 3. The final-norm import had no coverage in the default CTest path (only where a real pack is passed, and three of the four packs here ship no prefill). New crafted leg test_final_norm_envelope(): it builds the smallest pack that reaches the tail envelope (8 layers so the locator's six-anchor floor is met), asserts the container carries exactly the bf16 bytes at tail+64, and then the control -- corrupt the tail epsilon, and the norm must be refused BY NAME, nothing may be written without --allow-incomplete, and the container that is written must list the omission. 4. The runtime-refusal manifest merge (from #842's review) applies here too: a refused final norm now appears in the container's `incomplete` list instead of leaving `incomplete: []`. Verified: crafted leg prints "final norm: crafted tail envelope imported from tail+64, missing-epsilon control refused"; all four real packs pass the pack leg (rc 0); ctest 2/2; copyright rc 0.
|
Persistent review updated to latest commit 5bc07a3 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 2e9c5fa19 (the READY carries from 08fc9b03e; this commit is re-verified, pack leg included).
Re-ran the pack leg at this head (~/wt/tprs-842-build, cmake --build . --target npu_pack_import_test,
then ./npu_pack_import_test "" /home/bcloud/models/oga/<pack>):
Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen3-1.7B_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen3-4B_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
The three test controls are real fixes, and the first one was hiding a hole. pairs_even_odd = pairs_even_odd && … — the old assignment left only the last i of each block checked, so the 0.5B/4B attention-tile and MLP zero-point
pairing gates could pass with every earlier pair wrong. The gate/up swap control now tiles both streams
(for (const bool up : {false, true})) and compares sizes, so it can no longer "catch" a length difference as a swap.
The per-pid scratch dir is the shared-/tmp flake I reported (two concurrent runs deleting each other's
/tmp/oga_pack_import_test); ("oga_pack_import_test." + std::to_string(::getpid())) closes it.
Manifest merge — read, and the string contract checks out. p.refused() is built as t.name + ": " + t.refusal
(npu/pack_import.cpp:809), which is the ": " split the merge relies on for per-tensor attribution; the one
header-level refusal in npu/flm_head_import.cpp:143 has no name, and the merge's c == std::string::npos guard
correctly still lists it in incomplete without pretending to attribute it. w.finish(json::parse(manifest))
re-parsing the plan's JSON is fine.
One coverage note, not a gate: the new assertions did not execute in my four-pack run. They sit inside
if (!r.refused.empty()) (tests/npu_pack_import_test.cpp:734) and none of the four real packs refuses anything
(grep -c refused on all four logs: 0), so "lists every runtime refusal" / "imported=false" are unexercised here —
they only run for a pack that refuses by name. The golden leg already crafts refusals (crafted embed refusal: model.embed_tokens.weight: needs 4194304 B, declares 1024 in model.onnx.data), so a --allow-incomplete import of
that same crafted pack is the missing case.
For what it is worth, truncating a pack's model.onnx.data is not a way to produce a refusal: at 200 MB of 334 MB
the import throws FAIL exception: model.embed_tokens.weight: external data past the end of model.onnx.data, i.e.
it fails before the per-tensor refusal path. (I did that on a reflinked copy with a distinct inode — the shared pack
is untouched.)
CI: build + Analyze x4 + CodeQL SUCCESS, pr_agent cancelled by a superseding run, hw_gate SKIPPED. Merge order:
this before #844 (which carries it), as before.
…rown out of the importer
read_declared() iterated <pack>/cache directly, so a pack without that directory threw
std::filesystem_error out of the import and the harness printed
FAIL exception: filesystem error: directory iterator cannot open directory: .../cache
for the *hybrid* flavour of these packs, which ships model.bin instead of dd_metastate_*.fconst and has
no cache/ at all -- a different on-disk layout, not a broken one. Two changes, both about saying what
happened:
* read_declared(): a missing cache/ means no declared N/K, which is what an empty map already says;
the absence is reported once on stderr instead of throwing.
* the harness: a pack with no dd_metastate_* is recognised as the hybrid flavour, named, and the pack
leg is skipped -- the verdict line says "pack leg SKIPPED, not an OGA NPU pack" rather than claiming
a pass it did not earn.
Verified on the five packs on this box: the four real OGA packs still run the pack leg and pass rc=0
(Llama-3.2-1B, Qwen2.5-0.5B, Qwen3-1.7B, Qwen3-4B), and the hybrid pack now prints the skip and exits 0
instead of failing with a filesystem exception.
|
New commit Found by running the pack leg on all five pack directories on this box rather than the four I had been using. The fifth, which says nothing about what the pack is.
Re-verified on this head, all five directories:
For the record, my first check of this pack was a false green: I read |
|
Preparing review... |
|
Persistent review updated to latest commit 49baaee |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 49baaee85. Re-ran the whole five-pack sweep at this head and the new skip behaves exactly as
described — it names what it is instead of throwing, and the final line no longer reads as a pass.
Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen2.5-0.5B-Instruct_rai_1.8.0_hybrid rc=0 skip pack: … ships no dd_metastate_*.fconst, so it is the hybrid/model.bin
flavour of these packs and not an OGA NPU pack this importer reads
all npu-pack checks passed -- pack leg SKIPPED, not an OGA NPU pack
Qwen3-1.7B_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
Qwen3-4B_rai_1.8.0_npu_4K rc=0 all npu-pack checks passed (including the pack leg)
That is the shape of fix I asked for two reviews ago: four legs that actually run, one that says which pack it is and
why it is not being read, and a verdict line that cannot be mistaken for the four. read_declared() now returns an
empty map with a note instead of letting std::filesystem_error escape the importer, and the directory_iterator
also takes the error_code, so a mid-iteration failure cannot throw either.
Two notes, neither a gate:
read_declared()reports "has no cache/" for any reasonis_directoryfails — a pack with acache/that cannot
be opened (permissions, I/O) gets the same sentence and the same skipped cross-checks. The fallback is the safe
direction (fewer checks, loudly noted), but the message could carryec.message()whenecis set so the two
cases read differently.- Consequence worth knowing for whoever imports one directly: the harness now skips the hybrid pack entirely, so
the new "no cache ⇒ empty declarations" path insideimport_packis not exercised by any leg.1bit npu-pack importon that pack is the only way to reach it; if that flavour is meant to be supported, a crafted
no-cache/directory in the golden leg would cover it the way the crafted refusals cover the rest.
Everything I verified at 2e9c5fa19 (the manifest merge, the three fixed test controls, the per-pid scratch dir)
is unchanged by this commit, and the merge of main (5bc07a3) brings #934/#938/etc without touching the importer.
CI: build + Analyze ×4 + CodeQL green at this head.
…ed /tmp path pi-prs' note 2 on #936: key_on and key_off both redirected to /tmp/1bit-mint-selftest.log, so a vm-check row and a person running the selftest at the same time would interleave in one file - the same class as the /tmp/oga_pack_import_test flake on #842. $d/mint.log costs nothing and cannot collide. Measured: --mint-selftest still rc=0 (mint 1 line / 48 hex / 0600 / other lines kept; off: 0 lines, clean), and the scratch dir now holds lemond.conf + mint.log, nothing outside it.
…w (follow-up to #935's review notes 2 and 5) (#936) * os/v3/puppy: the mint path gets a selftest and a gate row, and "shown" keeps its shape guard Both are notes pi-prs left on #934 (items 2 and 5), both real: 2. Item 1's write path - the only new write in this image - could only be reached from a dialog, with CONF and the restart hard-coded, so nothing could drive it at a temp path. Now CONF=${ONEBIT_LEMOND_CONF:-/etc/1bit/lemond.conf} and LEMOND_INIT=${ONEBIT_LEMOND_INIT:-/etc/init.d/10lemonade} (defaults unchanged; a boot sets neither), and `1bit-console-menu --mint-selftest <dir>` drives key_on/key_off there with the restart replaced by `true`: asserts one 48-hex line, mode 600, the other lines kept, then no line and no <conf>.new left. A new vm-check row `U3 key toggle` runs it INSIDE the image, so the mint is a gate row rather than a manual test (the reason it is stacked behind #934: it changes an embedded file, so it needs its own image). 5. The read-only half printed `api-key-shown` for any non-empty value. A hand-edited or glued line can hold a fragment, so the marker is back behind the >=32-character guard while the key itself is still printed (release-check and menugate read the value, not the marker). Measured here: --mint-selftest rc=0 (`mint: lines=1 hex=48 mode=600 other-lines-kept=1` / `off: lines=0 clean` / `mint-selftest-ok`), and the first run of it failed on `|| echo 0` making grep's count "0\n0" - fixed and noted in the file. sh -n / bash -n / embedded-python compile / copyright rc=0, shellcheck adds nothing. * os/v3/puppy: the mint selftest logs inside its scratch dir, not a fixed /tmp path pi-prs' note 2 on #936: key_on and key_off both redirected to /tmp/1bit-mint-selftest.log, so a vm-check row and a person running the selftest at the same time would interleave in one file - the same class as the /tmp/oga_pack_import_test flake on #842. $d/mint.log costs nothing and cannot collide. Measured: --mint-selftest still rc=0 (mint 1 line / 48 hex / 0600 / other lines kept; off: 0 lines, clean), and the scratch dir now holds lemond.conf + mint.log, nothing outside it. --------- Co-authored-by: pi-os3 <pi-os3@1bit.local> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
|
Persistent review updated to latest commit e2ce4d5 |
|
Persistent review updated to latest commit 2cd33d4 |
|
Persistent review updated to latest commit 39a3a81 |
|
Persistent review updated to latest commit c4d8dfc |
|
Preparing review... |


What
The MLP section of an OGA pack's layer block — the last part of the pack the importer refused (72 projections on Qwen2.5-0.5B) — is now decoded, byte-verified and imported. After this, the only tensor
1bit npu-packstill refuses ismodel.norm(final), whose position is genuinely unestablished.The layout, and why it is a measurement
Derived from the writer's own headers (
mladfQuantMatrix/mladfWgtSubvin the vendor wheel'sops_common/mladf_matmul_matrix.hpp,concat_gate_upinmatmulbias_weights_gateup.cpp), then checked against the bytes:gate+uphidden x 2*intermediatetensor, rows interleaved in 32-column chunks (gate 32, up 32, …), in 128-column groups of 4,288-byte subvolumes — the same struct atNsubv 64:[0,4096)quants,[4096,4160)zero points,[4160,4288)64 bf16 scales01 00 …), the same bytes in every layer blockdown_projintermediate x hiddenin 64-column groups of the 2,176-byte subvolume the attention payload already usesThe three parts must fill the block stride exactly, and that identity — not a constant — decides whether a pack is one this decoder understands. On Qwen2.5-0.5B they end at 8,708,288 B, the measured stride, to the byte. Qwen3-1.7B and Qwen3-4B do not add up and refuse by name with the size they did add up to (their writer used different kernel shapes — the same runtime-shapes table engine#734 ran into).
Code
The same decoder at a second width.
TileWgainsrows(64 or 128 columns); the subvolume size, its zero/scale offsets and its quant index follow from it, so the attention payload,down_projand the fused width share one verified code path.gu_half()splits a fused 128-column group into the two projections (gate = rows 0..31 and 64..95, up = 32..63 and 96..127).Verified on the shipped pack
Nsubv 64while the prefill program stores them separately atNsubv 32, and the two agree on codes, scales and zero points for all 24 layers × {gate, up, down}.tests/golden/oga_mlp_gu_group.bin— one 68,608-byte unit copied verbatim from the pack bytools/oga_mlp_gu_golden.py, plus the container tiles its two projections give — so the decode and the split are checked against the vendor writer's own bytes where no vendor stack exists, with two controls that must fail (a moved code nibble, a one-ulp scale).Modelopens it.The bf16 parameter bound is unchanged and reported, not hidden: zero points are bf16-exact for 52.9 % of the MLP's 32-column groups (48.7 % of the attention ones), residual 3.34e-3 relative.
Checks run
On this branch, fresh
Release/ninja/gcc-16 configure withONEBIT_OGA_PACK:tools/copyright.py --check→ rc 0ctest -R npu_pack_import→ 2/2 (npu_pack_import0.00 s incl. the new golden,npu_pack_import_oga2.05 s on the real pack)tools/oga_mlp_gu_golden.py <pack> --check→ the fixtures re-derive from the packNot in this PR
model.norm(final) stays refused: its.fconsttail is[eps][pad][448 f32], not an RMSNorm weight. The MLP geometry for other hidden sizes is open (refused by name, with the arithmetic in the message). Nothing of the vendor's is linked, called or shipped — the reader is written from the documented format facts inamd-oss/npu-re/docs/FACTS-oga-pack-weight-layout.md§7.17.